package com.jintian.smart.kernel.module.impl;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.jintian.smart.kernel.common.util.StringUtil;
import com.jintian.smart.kernel.module.Model;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.beetl.sql.clazz.kit.ClassLoaderKit;
import org.beetl.sql.core.ConditionalSQLManager;
import org.beetl.sql.core.SQLManager;
import org.beetl.sql.core.loader.MarkdownClasspathLoader;
import org.beetl.sql.core.loader.SQLLoader;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.*;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.Map.Entry;

@Data
@Slf4j
public class ModelImpl implements Model {
    private String mainClass;
    private String name;

    @Setter(AccessLevel.NONE)
    @JsonIgnore
    private File root;

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    @JsonIgnore
    private ClassLoader staticLoader;

    @JsonIgnore
    @Setter(AccessLevel.NONE)
    private ModelImpl parent;

    @JsonIgnore
    private ApplicationContext parentContext;

    @JsonIgnore
    @Setter(AccessLevel.NONE)
    private ResourceLoader resourceLoader;

    @JsonIgnore
    @Setter(AccessLevel.NONE)
    private ModelApplicationContext modelContext = new ModelApplicationContext();

    @Setter(AccessLevel.NONE)
    private final Map<String, ModelImpl> children = new HashMap<String, ModelImpl>();

    private boolean _init0;

    private boolean _init;

    private boolean dev;

    private String beetlTemplatesPath = "templates";
    private String beetlViewSuffix = "html";
    private String daoSuffix = "Mapper";
    private List<Mapper> mappers;
    private Map<String, String> resourceLocations;
    private String componentScan;

    private List<Object> registeredControllers = new ArrayList<>();

    private String version = "" + System.currentTimeMillis();

    private String sourceCodeBasePackage;

    private String sourceCodeRoot;

    public Model load(File file) throws Exception {
        this.root = file.getParentFile();
        try (InputStream in = new FileInputStream(file)) {
            XStream xstream = new XStream(new DomDriver());
            xstream.alias("model", ModelImpl.class);
            xstream.alias("mappers", ArrayList.class);
            xstream.alias("mapper", Mapper.class);
            xstream.alias("resourceLocations", HashMap.class);
            xstream.alias("resourceLocation", Entry.class);
            xstream.alias("url-pattern", String.class);
            xstream.alias("locations", String.class);

            xstream.fromXML(in, this);
        }
        if (StringUtil.isEmpty(this.beetlViewSuffix)) {
            this.beetlViewSuffix = "html";
        }
        _init0();
        return this;
    }

    private void _init0() {
        if (this._init0) {
            return;
        }
        log.debug("initial model :" + this.getName());
        this.modelContext.setModel(this);
        ArrayList<URL> urlList = new ArrayList<URL>();
        try {
            urlList.add(this.root.toURI().toURL());
            File file = new File(this.root, "conf");
            if (file.exists()) {
                urlList.add(file.toURI().toURL());
            }
            file = new File(this.root, "classes");
            if (file.exists()) {
                urlList.add(file.toURI().toURL());
            }
        } catch (MalformedURLException e) {
            log.error("load {} error,{}", this.root, e.getMessage());
        }
        try {
            Files.find(Paths.get(this.root.getPath(), "lib"), 1, (path, attr) -> {
                return !attr.isDirectory() && path.toFile().getName().endsWith(".jar");
            }, FileVisitOption.FOLLOW_LINKS).map(path -> {
                try {
                    return path.toUri().toURL();
                } catch (Exception e) {
                    return null;
                }
            }).forEach(url -> urlList.add(url));
        } catch (Exception e) {
            log.error("load {} error,{}", Paths.get(this.root.getPath(), "lib"), e.getMessage());
        }
        this.staticLoader = new ModelClassLoader(this, urlList.toArray(new URL[urlList.size()]), this.parentContext == null ? null : this.parentContext.getClassLoader());

        this._init0 = true;
        if (this.parent != null) this.parent.children.put(getName(), this);
    }

    private void loadModelYaml(ModelEnvironment environment, String name, ResourceLoader resourceLoader) {
        List<Resource> resources = new ArrayList<>();
        Resource resource = resourceLoader.getResource("classpath:application.yml");
        if (resource.exists()) {
            resources.add(resource);
        }
        YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
        File[] files = new File(this.root, "/conf").listFiles();

        if (files != null) {
            for (File file : files) {
                if (file.getName().toLowerCase().endsWith(".yml")) {
                    try {
                        resources.add(new FileUrlResource(file.toURI().toURL()));
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        yamlPropertiesFactoryBean.setResources(resources.toArray(new Resource[0]));
        Properties properties = yamlPropertiesFactoryBean.getObject();
        PropertiesPropertySource ps = new PropertiesPropertySource(name, properties);
        environment.getPropertySources().addLast(ps);
    }

    @Override
    public ClassLoader getClassLoader() {
        return this.staticLoader;
    }

    private void refreshModelContext(Class<?> modelMainClazz) {
        ResourceLoader resourceLoader = new DefaultResourceLoader(this.staticLoader);
        ModelEnvironment environment = new ModelEnvironment();
        this.loadModelYaml(environment, "model-" + name + "-conf", resourceLoader);

        {
            Map<String, Object> map = new HashMap<>();
            // 1.设置模块名称，用于子系统controller前缀
            map.put(Model.PROPERTY_MODEL_NAME, this.getName());
            // 2.关闭beetl自动配置
            map.put("beetl.enabled", "false");

            // 3.关闭主从环境
            map.put(Model.PROPERTY_SMART_MASTER, "false");
            if (this.dev) {
                map.put("spring.devtools.restart.enabled", "true");
            }
            environment.getPropertySources().addLast(new MapPropertySource("model-" + name, map));
        }

        this.resourceLoader = resourceLoader;
        this.modelContext.setEnvironment(environment);
        ((AnnotationConfigApplicationContext) this.modelContext).setResourceLoader(resourceLoader);
        this.modelContext.setClassLoader(this.staticLoader);
        this.modelContext.setParent(this.parentContext);
        ModelBeetlProcessor processor = new ModelBeetlProcessor(this);
        processor.setApplicationContext(this.modelContext);
        this.modelContext.addBeanFactoryPostProcessor(processor);
        this.modelContext.register(modelMainClazz);
    }

    public synchronized void init() {
        log.info("======> 开始初始化model");
        if (this._init) {
            return;
        }
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(this.staticLoader);
            Class<?> clazz = null;
            try {
                clazz = this.staticLoader.loadClass(this.mainClass);
            } catch (ClassNotFoundException e) {
                log.error("start {} error,not found {}", this.getName(), this.getMainClass());
                return;
            }
            if (StringUtil.isEmpty(this.componentScan)) {
                this.componentScan = clazz.getPackage().getName();
            }
            this.refreshModelContext(clazz);
            this._init = true;
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }
    }

    @Override
    public synchronized void start() {
        log.info("\n**************************************************\n*启动模块{}={},{}\n************************************************** ", this.getName(), this.mainClass, this.root);
        if (this.modelContext.isActive()) {
            log.info("\n {}已经启动，不再加载", this.getName());
            return;
        }

        try {
            String[] names = this.parentContext.getBeanNamesForType(SQLManager.class);
            log.info("\n 获取到SQLManager：{} ", String.join(",", names));
            for (String name : names) {
                log.info("\n 开始加载SQLManager：{} ", name);
                SQLManager manager = this.parentContext.getBean(name, SQLManager.class);
                // SQLManager的ClassLoaderKit增加模块的classLoader，用于创建模块Mapper
                ClassLoaderKit classLoaderKit = manager.getClassLoaderKit();
                Field field = ReflectionUtils.findField(classLoaderKit.getClass(), "otherLoaders");
                field.setAccessible(true);
                @SuppressWarnings("unchecked") List<ClassLoader> classLoaders = (List<ClassLoader>) ReflectionUtils.getField(field, classLoaderKit);
                classLoaders.add(this.staticLoader);
                if (!(manager instanceof ConditionalSQLManager)) {
                    SQLLoader sqlloader = manager.getSqlLoader();
                    // 替换SQLLoader的ClassLoaderKit，用于加载模块的sql.md文件
                    if (sqlloader instanceof MarkdownClasspathLoader) {
                        ((MarkdownClasspathLoader) sqlloader).setClassLoaderKit(classLoaderKit);
                    }
                }
                log.info("\n SQLManager完成加载：{} ", name);
            }
            this.modelContext.refresh();// BeetlSqlClassPathScanner
            log.info("\n 注册controller");
            this.registerControllerToParent();
        } catch (Exception ex) {
            log.error("{}启动失败", this.getName(), ex);
        }
    }

    private void registerControllerToParent() {
        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) this.parentContext.getBean("requestMappingHandlerMapping");
        ApplicationContext old = requestMappingHandlerMapping.getApplicationContext();
        // 将子模块的PropertySources添加到父环境最开始
        StandardEnvironment pe = (StandardEnvironment) this.parentContext.getEnvironment();
        MutablePropertySources modelPropSources = ((ModelEnvironment) this.modelContext.getEnvironment()).getPropertySources();
        List<PropertySource<?>> modelPropSourcesList = new ArrayList<>();
        modelPropSources.forEach(propertySource -> modelPropSourcesList.add(propertySource));
        for (int idx = modelPropSourcesList.size() - 1; idx >= 0; idx--) {
            pe.getPropertySources().addFirst(modelPropSourcesList.get(idx));
        }

        Field field = ReflectionUtils.findField(requestMappingHandlerMapping.getClass(), "applicationContext");
        try {
            field.setAccessible(true);
            try {
                field.set(requestMappingHandlerMapping, modelContext);
            } catch (Exception e) {
                e.printStackTrace();
            }
            requestMappingHandlerMapping.setApplicationContext(modelContext);
            this.registerControllerToParent(requestMappingHandlerMapping, Controller.class);
        } finally {
            // 移除子模块的PropertySources
            for (PropertySource<?> propertySource : modelPropSourcesList) {
                pe.getPropertySources().replace(propertySource.getName(), propertySource);
            }
            try {
                field.set(requestMappingHandlerMapping, old);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void registerControllerToParent(RequestMappingHandlerMapping requestMappingHandlerMapping, Class<? extends Annotation> annotationType) {
        String[] beans = modelContext.getBeanNamesForAnnotation(annotationType);
        List<String> info = new ArrayList<>();
        for (String name : beans) {
            Object controller = modelContext.getBean(name);
            Class<?> objClass = modelContext.getType(name);
            if (!objClass.getName().startsWith(this.componentScan)) continue;
            try {
                Method method = requestMappingHandlerMapping.getClass().getSuperclass().getSuperclass().getDeclaredMethod("detectHandlerMethods", Object.class);
                method.setAccessible(true);
                // 使用beanName进行注册，这样HandlerMethod中包含beanFactory
                method.invoke(requestMappingHandlerMapping, name);
                registeredControllers.add(controller);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            Class<?> targetClass = ClassUtils.getUserClass(controller.getClass());
            ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() {
                @Override
                public void doWith(Method method) {
                    Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
                    try {
                        Method createMappingMethod = RequestMappingHandlerMapping.class.getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
                        createMappingMethod.setAccessible(true);
                        RequestMappingInfo requestMappingInfo = (RequestMappingInfo) createMappingMethod.invoke(requestMappingHandlerMapping, specificMethod, targetClass);
                        if (requestMappingInfo != null) {
                            Set<String> patterns = requestMappingInfo.getPatternValues();
                            info.add(String.format("%s \t\t%s.%s", StringUtil.join(patterns, ","), targetClass.getSimpleName(), specificMethod.getName()));
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }, ReflectionUtils.USER_DECLARED_METHODS);
        }
        Collections.sort(info);
        log.info("RequestMappingHandlerMapping:\n{}", StringUtil.join(info, "\n"));
    }

    private void unregisterControllerFormParent() {
        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) this.parentContext.getBean("requestMappingHandlerMapping");
        Iterator<Object> itro = registeredControllers.iterator();
        while (itro.hasNext()) {
            Object controller = itro.next();
            itro.remove();
            Class<?> targetClass = ClassUtils.getUserClass(controller.getClass());
            ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() {
                @Override
                public void doWith(Method method) {
                    Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
                    try {
                        Method createMappingMethod = RequestMappingHandlerMapping.class.getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
                        createMappingMethod.setAccessible(true);
                        RequestMappingInfo requestMappingInfo = (RequestMappingInfo) createMappingMethod.invoke(requestMappingHandlerMapping, specificMethod, targetClass);
                        if (requestMappingInfo != null) {
                            Set<String> patterns = requestMappingInfo.getPatternValues();
                            log.info("注销接口：{}", patterns);
                            requestMappingHandlerMapping.unregisterMapping(requestMappingInfo);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }, ReflectionUtils.USER_DECLARED_METHODS);
        }
    }

    @Override
    public synchronized void stop() {
        if (this.modelContext == null) return;
        this.unregisterControllerFormParent();
        String[] names = this.parentContext.getBeanNamesForType(SQLManager.class);
        for (String name : names) {
            SQLManager manager = this.parentContext.getBean(name, SQLManager.class);
            ClassLoaderKit classLoaderKit = manager.getClassLoaderKit();
            Field field = ReflectionUtils.findField(classLoaderKit.getClass(), "otherLoaders");
            field.setAccessible(true);
            @SuppressWarnings("unchecked") List<ClassLoader> classLoaders = (List<ClassLoader>) ReflectionUtils.getField(field, classLoaderKit);
            classLoaders.remove(this.staticLoader);
        }
        try {
            this.modelContext.stop();
        } catch (Exception ex) {
        }

        this.modelContext.close();
        this.modelContext = null;
        this._init0 = false;
        this._init = false;
    }

    @Override
    public synchronized void restart() {
        this.stop();
        this.modelContext = new ModelApplicationContext();
        this._init0();
        this.init();
        this.start();
    }

    public static void main(String[] args) throws Exception {
        ModelEnvironment environment = new ModelEnvironment();

        Map<String, Object> map = new HashMap<>();
        map.put("spring.autoconfigure.exclude", "com.jintian.mda.autoconfigure.flow.FlowableStarter");
        environment.getPropertySources().addLast(new MapPropertySource("model-test", map));
        Binder binder = Binder.get(environment);
        List<String> ttt = binder.bind("spring.autoconfigure.exclude", String[].class).map(Arrays::asList).orElse(Collections.emptyList());
        System.out.println(ttt);
    }

    @Override
    public Map<String, Resource[]> getResourceHandlerLocations() {
        Map<String, Resource[]> handlerLocations = new HashMap<>();
        if (this.resourceLocations != null) {
            for (Entry<String, String> e : this.resourceLocations.entrySet()) {
                String[] locations = e.getValue().split(",");
                List<Resource> resources = new ArrayList<>();
                for (String location : locations) {
                    if (StringUtil.isNotEmpty(location)) {
                        Resource r = this.resourceLoader.getResource(location);
                        if (r != null) {
                            resources.add(r);
                        }
                    }
                }
                if (resources.size() > 0) {
                    handlerLocations.put(e.getKey(), resources.toArray(new Resource[0]));
                }
            }
        }
        return handlerLocations;
    }

    @Override
    public ResourceLoader getResourceLoader() {
        return this.resourceLoader;
    }

    static class ModelEnvironment extends AbstractEnvironment {
    }
}
